// Copyright 2024 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;
import 'package:web_benchmarks/analysis.dart';

import 'args.dart';
import 'run_benchmarks.dart';
import 'utils.dart';

/// This is a helper script to perform a comparison of dart2wasm peformance
/// with dart2js performance for the full set of DevTools benchmark tests.
///
/// This script:
///
/// 1. Runs the benchmarks with dart2js, averaging the data over a fixed number
///    of test runs (defaults to 1 and can be set using the '--average-of' arg).
///    The average dart2js data is used as baseline data for step #3.
/// 2. Runs the benchmarks with dart2wasm, averaging the data over a fixed
///    number of test runs (defaults to 1 and can be set using the
///    '--average-of' arg). The average dart2wasm data is used as the test data
///    for step #3.
/// 3. Computes the delta between the dart2wasm results (step #2) and the
///    dart2js results (step #1).
/// 4. Outputs the data to a .csv file that can be easily opened in a
///    spreadsheet application (Google Sheets, Excel, etc.) for viewing. By
///    default, this file will be saved to the
///    '~/Downloads/devtools_benchmark_data/' directory with an automatically
///    generated file name. A different output path can be specified using
///    the '--save-to-file' arg.
///
/// Optionally, a previously generated benchmark data file can be used for
/// either or both the dart2js baseline data (normally generated from step #1)
/// and the dart2wasm test data (normally generated from step #2). To use an
/// existing benchmark run, specify the absolute path to the benchmark data
/// using the '--baseline' or '--test' arguments.
///
/// Example usage:
///  * dart run benchmark/scripts/dart2wasm_performance_diff.dart
///
/// Example usage that averages benchmark results over 5 runs:
///  * dart run benchmark/scripts/dart2wasm_performance_diff.dart --average-of=5
///
/// Example usage that diffs against an existing basline:
///  * dart run benchmark/scripts/dart2wasm_performance_diff.dart --baseline=/Users/me/Downloads/baseline_run.json
void main(List<String> args) async {
  if (!Directory.current.path.contains('devtools_app')) {
    stderr.writeln(
      'This script must be ran from the devtools_app/ directory or one of its '
      'sub-directories.',
    );
    return;
  }

  if (args.isNotEmpty && args.first == '-h') {
    stdout.writeln(_Args._buildArgParser().usage);
    return;
  }

  final scriptArgs = _Args(args);
  final averageOf = scriptArgs.averageOf;

  // Run new benchmarks or parse existing results, and compute the delta.
  final baseline = await runBenchmarkOrUseExisting(
    scriptArgs.baselineLocation,
    averageOf: averageOf,
    useWasm: false,
  );
  final test = await runBenchmarkOrUseExisting(
    scriptArgs.testLocation,
    averageOf: averageOf,
    useWasm: true,
  );
  final delta = computeDelta(baseline, test);

  // Generate the CSV file and download it.
  final csvBuilder = CsvBuilder(saveToLocation: scriptArgs.saveToFileLocation)
    ..writeHeaders(averageOf: averageOf);
  delta.toCsvLines().forEach(csvBuilder.writeLine);
  csvBuilder.download();
}

class CsvBuilder {
  CsvBuilder({this.saveToLocation})
    : assert(saveToLocation == null || saveToLocation.endsWith('.csv'));

  final _sb = StringBuffer();

  final String? saveToLocation;

  void writeHeaders({required int averageOf}) {
    writeLines([
      'Flutter DevTools performance benchmarks diff: dart2wasm diffed against dart2js.',
      'Benchmark results were averaged over $averageOf benchmark run(s).',
      '',
      'These results were auto-generated by a script:',
      'https://github.com/flutter/devtools/blob/master/packages/devtools_app/benchmark/scripts/dart2wasm_performance_diff.dart',
      '',
    ]);

    // Write the Flutter and DevTools commit hash for the benchmark run.
    // TODO(kenz): automatically detect these and write them to the CSV.
    const flutter = '<enter manually by running \'flutter --version\'>';
    const devtools = '<enter manually by running \'git log\'>';
    writeLines([
      'Version info:',
      'Flutter: $flutter',
      'DevTools: $devtools',
      '',
      'Results:',
    ]);

    // Write the headers.
    writeLine([
      'Benchmark Name',
      'Metric',
      'Value (micros)',
      'Delta (micros)',
      'Delta (%)',
    ]);
  }

  void writeLine(List<String> content) {
    _sb.writeln(convertToCsvLine(content));
  }

  void writeLines(List<String> lines) {
    for (final line in lines) {
      writeLine([line]);
    }
  }

  /// Downloads the current [CsvBuilder] content [_sb] to disk.
  void download() {
    Uri? saveToUri = saveToLocation != null ? Uri.parse(saveToLocation!) : null;
    if (saveToUri == null) {
      final time = DateTime.now();
      final timestamp = DateFormat('yyyy_MM_dd-HH_mm_ss').format(time);
      final fileName = p.join(
        'devtools_benchmark_data',
        'dart2wasm_performance_diff_$timestamp.csv',
      );

      // Try to use the Downloads directory.
      // TODO(kenz): make this code possible to run on Windows if necessary.
      final currentDirectoryParts = Directory.current.uri.pathSegments;
      final downloadsDir = Directory.fromUri(
        Uri.parse(
          // We need the leading slash so that this is an absolute path.
          '/${p.join(currentDirectoryParts[0], currentDirectoryParts[1], 'Downloads')}',
        ),
      );
      if (downloadsDir.existsSync()) {
        saveToUri = Uri.parse(p.join(downloadsDir.uri.path, fileName));
      } else {
        stderr.writeln(
          'Warning: could not locate the \'Downloads\' directory at '
          '${downloadsDir.path}. Saving results to the system temp directory '
          'instead.',
        );
        saveToUri = Uri.parse(p.join(Directory.systemTemp.uri.path, fileName));
      }
    }

    final file = File.fromUri(saveToUri)..createSync(recursive: true);
    file.writeAsStringSync(_sb.toString(), flush: true);

    stdout
      ..writeln('Wrote dart2wasm performance diff to ${file.absolute.path}.')
      ..writeln(
        'Open the file in a spreadsheet application '
        '(Google Sheets, Excel, etc.) for viewing.',
      );
  }
}

Future<BenchmarkResults> runBenchmarkOrUseExisting(
  String? existingBenchmarkLocation, {
  required int averageOf,
  required bool useWasm,
}) async {
  if (existingBenchmarkLocation == null) {
    // Run the benchmark [averageOf] times and take the average.
    return await runBenchmarks(
      averageOf: averageOf,
      useWasm: useWasm,
      useBrowser: false,
    );
  } else {
    final existingBenchmarkFile = checkFileExists(existingBenchmarkLocation);
    if (existingBenchmarkFile == null) {
      throw const FileSystemException('Benchmark file does not exist.');
    } else {
      return BenchmarkResults.parse(
        jsonDecode(existingBenchmarkFile.readAsStringSync()),
      );
    }
  }
}

class _Args extends BenchmarkArgsBase {
  _Args(List<String> args) {
    init(args, parser: _buildArgParser());
  }

  String? get testLocation => argResults[BenchmarkArgument.test.flagName];

  static ArgParser _buildArgParser() {
    return ArgParser()
      ..addSaveToFileOption(BenchmarkResultsOutputType.csv)
      ..addAverageOfOption()
      ..addBaselineOption(
        additionalHelp:
            'When specified, this script will use the benchmark data at '
            'the specified path as the baseline instead of generating a new '
            'benchmark run for the dart2js baseline. This file path should '
            'point to a JSON file that was created by running the '
            '`run_benchmarks.dart` script for the dart2js build of DevTools.',
      )
      ..addOption(
        BenchmarkArgument.test.flagName,
        help:
            'The test benchmark data (dart2wasm) to use for this performance'
            ' diff. When specified, this script will use the benchmark data at '
            'the specified path instead of generating a new benchmark run for '
            'the dart2wasm data. This file path should point to a JSON file that '
            'was created by running the `run_benchmarks.dart` script with the '
            '`--wasm` flag.',
        valueHelp: '/Users/me/Downloads/dart2wasm_data.json',
      );
  }
}
